]> git.saurik.com Git - apple/security.git/blob - OSX/Keychain Circle Notification/KNAppDelegate.m
Security-57740.1.18.tar.gz
[apple/security.git] / OSX / Keychain Circle Notification / KNAppDelegate.m
1 /*
2 * Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23
24
25 #import "KNAppDelegate.h"
26 #import "KDSecCircle.h"
27 #import "KDCirclePeer.h"
28 #import "NSDictionary+compactDescription.h"
29 #import <AOSUI/NSImageAdditions.h>
30 #import <AppleSystemInfo/AppleSystemInfo.h>
31 #import <Security/SecFrameworkStrings.h>
32
33 #import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
34 #import <AOSAccounts/MobileMePrefsCore.h>
35
36 #include <msgtracer_client.h>
37 #include <msgtracer_keys.h>
38 #include <CrashReporterSupport/CrashReporterSupportPrivate.h>
39 #import "CoreCDP/CDPFollowUpController.h"
40 #import "CoreCDP/CDPFollowUpContext.h"
41
42 static const char * const kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK";
43 static const NSString * const kKickedOutKey = @"KickedOut";
44 static const NSString * const kValidOnlyOutOfCircleKey = @"ValidOnlyOutOfCircle";
45
46
47 @implementation KNAppDelegate
48
49 static NSUserNotificationCenter *appropriateNotificationCenter()
50 {
51 return [NSUserNotificationCenter _centerForIdentifier: @"com.apple.security.keychain-circle-notification"
52 type: _NSUserNotificationCenterTypeSystem];
53 }
54
55
56 - (void) notifyiCloudPreferencesAbout: (NSString *) eventName
57 {
58 if (eventName == nil)
59 return;
60
61 NSString *account = (__bridge NSString *)(MMCopyLoggedInAccount());
62 NSLog(@"notifyiCloudPreferencesAbout %@", eventName);
63
64 AEDesc aeDesc;
65 BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *) kMMServiceIDKeychainSync, eventName, account, &aeDesc);
66 if (createdAEDesc) {
67 NSArray *prefPaneURL = [NSArray arrayWithObject: [NSURL fileURLWithPath:@"/System/Library/PreferencePanes/iCloudPref.prefPane"]];
68
69 LSLaunchURLSpec lsSpec = {
70 .appURL = NULL,
71 .itemURLs = (__bridge CFArrayRef)prefPaneURL,
72 .passThruParams = &aeDesc,
73 .launchFlags = kLSLaunchDefaults | kLSLaunchAsync,
74 .asyncRefCon = NULL,
75 };
76 OSErr err = LSOpenFromURLSpec(&lsSpec, NULL);
77
78 if (err)
79 NSLog(@"Can't send event %@, err=%d", eventName, err);
80 AEDisposeDesc(&aeDesc);
81 } else {
82 NSLog(@"unable to create and send aedesc for account: '%@' and action: '%@'\n", account, eventName);
83 }
84 }
85
86
87 - (void) showiCloudPreferences
88 {
89 static NSAppleScript *script = nil;
90 if (!script) {
91 static NSString *script_src = @"tell application \"System Preferences\"\n"
92 "activate\n"
93 "set the current pane to pane id \"com.apple.preferences.icloud\"\n"
94 "end tell";
95 script = [[NSAppleScript alloc] initWithSource: script_src];
96 }
97
98 NSDictionary *scriptError = nil;
99 [script executeAndReturnError:&scriptError];
100
101 if (scriptError)
102 NSLog(@"scriptError: %@", scriptError);
103 else
104 NSLog(@"showiCloudPreferences success");
105 }
106
107
108 - (void) timerCheck
109 {
110 NSDate *nowish = [NSDate new];
111
112 self.state = [KNPersistentState loadFromStorage];
113 if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) {
114 NSLog(@"REMINDER TIME: %@ >>> %@", nowish, self.state.pendingApplicationReminder);
115
116 // self.circle.rawStatus might not be valid yet
117 if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) {
118 // Still have a request pending, send reminder, and also in addtion to the UI
119 // we need to send a notification for iCloud pref pane to pick up
120 CFNotificationCenterPostNotificationWithOptions(
121 CFNotificationCenterGetDistributedCenter(),
122 CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"),
123 (__bridge const void *) [self.state.applicationDate description], NULL, 0
124 );
125
126 [self postApplicationReminder];
127 self.state.pendingApplicationReminder = [nowish dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]];
128 [self.state writeToStorage];
129 }
130 }
131 }
132
133
134 - (void) scheduleActivityAt: (NSDate *) time
135 {
136 if ([time compare:[NSDate distantFuture]] != NSOrderedSame) {
137 NSTimeInterval howSoon = [time timeIntervalSinceNow];
138 if (howSoon > 0)
139 [self scheduleActivityIn:ceil(howSoon)];
140 else
141 [self timerCheck];
142 }
143 }
144
145
146 - (void) scheduleActivityIn: (int) alertInterval
147 {
148 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
149 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
150 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
151 xpc_dictionary_set_bool (options, XPC_ACTIVITY_REPEATING, false);
152 xpc_dictionary_set_bool (options, XPC_ACTIVITY_ALLOW_BATTERY, true);
153 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
154
155 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
156 [self timerCheck];
157 });
158 }
159
160
161 - (NSTimeInterval) getPendingApplicationReminderInterval
162 {
163 if (self.state.pendingApplicationReminderInterval)
164 return [self.state.pendingApplicationReminderInterval doubleValue];
165 else
166 return 24*60*60;
167 }
168
169
170 // Copied from sysdiagnose/src/utils.m
171 bool isAppleInternal(void)
172 {
173 static bool ret = false;
174 static dispatch_once_t onceToken;
175 dispatch_once(&onceToken, ^{
176 #if TARGET_OS_IPHONE
177 ret = CRIsAppleInternal();
178 #else
179 ret = CRHasBeenAppleInternalRecently();
180 #endif
181 });
182 return ret;
183 }
184
185
186 #define ICKC_EVENT_DISABLED "com.apple.security.secureobjectsync.disabled"
187 #define ICKC_EVENT_DEPARTURE_REASON "com.apple.security.secureobjectsync.departurereason"
188 #define ICKC_EVENT_NUM_PEERS "com.apple.security.secureobjectsync.numcircledevices"
189
190 - (void) applicationDidFinishLaunching: (NSNotification *) aNotification
191 {
192 appropriateNotificationCenter().delegate = self;
193 NSLog(@"Posted at launch: %@", appropriateNotificationCenter().deliveredNotifications);
194
195 self.viewedIds = [NSMutableSet new];
196 self.circle = [KDSecCircle new];
197 // self.state = [KNPersistentState loadFromStorage];
198 KNAppDelegate *me = self;
199
200 [self.circle addChangeCallback:^{
201 NSLog(@"{ChangeCallback}");
202 /* SOSCCStatus circleStatus = SOSCCThisDeviceIsInCircle(&error);
203 NSDate *nowish = [NSDate date];
204 PersistentState *state = [PersistentState loadFromStorage];
205 enum DepartureReason departureReason = SOSCCGetLastDepartureReason(&departError); */
206 // me.circle.rawStatus = SOSCCThisDeviceIsInCircle(&error);
207 NSDate *nowish = [NSDate date];
208 SOSCCStatus circleStatus = me.circle.rawStatus;
209 me.state = [KNPersistentState loadFromStorage];
210
211
212 // Pending application reminder
213 NSLog(@"{ChangeCallback} scheduleActivity %@", me.state.pendingApplicationReminder);
214 if (circleStatus == kSOSCCRequestPending)
215 [me scheduleActivityAt:me.state.pendingApplicationReminder];
216
217
218 // No longer in circle?
219 if ((me.state.lastCircleStatus == kSOSCCInCircle && (circleStatus == kSOSCCNotInCircle || circleStatus == kSOSCCCircleAbsent)) ||
220 (me.state.lastCircleStatus == kSOSCCCircleAbsent && circleStatus == kSOSCCNotInCircle && me.state.absentCircleWithNoReason) ||
221 me.state.debugLeftReason) {
222 enum DepartureReason reason = kSOSNeverLeftCircle;
223 if (me.state.debugLeftReason) {
224 reason = [me.state.debugLeftReason intValue];
225 me.state.debugLeftReason = nil;
226 [me.state writeToStorage];
227 } else {
228 CFErrorRef err = NULL;
229 reason = SOSCCGetLastDepartureReason(&err);
230 if (reason == kSOSDepartureReasonError) {
231 NSLog(@"SOSCCGetLastDepartureReason err: %@", err);
232 }
233 if (err) CFRelease(err);
234 }
235
236 if (reason != kSOSDepartureReasonError) {
237 // Post kick-out alert
238
239 // <rdar://problem/20862435> MessageTracer data to find out how many users were dropped & reset
240 msgtracer_domain_t domain = msgtracer_domain_new(ICKC_EVENT_DISABLED);
241 msgtracer_msg_t mt_msg = NULL;
242
243 if (domain != NULL)
244 mt_msg = msgtracer_msg_new(domain);
245
246 if (mt_msg) {
247 char s[16];
248
249 msgtracer_set(mt_msg, kMsgTracerKeySignature, ICKC_EVENT_DEPARTURE_REASON);
250 snprintf(s, sizeof(s), "%u", reason);
251 msgtracer_set(mt_msg, kMsgTracerKeyValue, s);
252
253 int64_t num_peers = 0;
254 CFArrayRef peerList = SOSCCCopyPeerPeerInfo(NULL);
255 if (peerList) {
256 num_peers = CFArrayGetCount(peerList);
257 if (num_peers > 99) {
258 // Round down # peers to 2 significant digits
259 int factor;
260 for (factor = 10; num_peers >= 100*factor; factor *= 10) ;
261 num_peers = (num_peers / factor) * factor;
262 }
263 CFRelease(peerList);
264 }
265 msgtracer_set(mt_msg, kMsgTracerKeySignature2, ICKC_EVENT_NUM_PEERS);
266 snprintf(s, sizeof(s), "%lld", num_peers);
267 msgtracer_set(mt_msg, kMsgTracerKeyValue2, s);
268
269 msgtracer_set(mt_msg, kMsgTracerKeySummarize, "NO");
270 msgtracer_log(mt_msg, ASL_LEVEL_DEBUG, "");
271 }
272
273 // FIXME:
274 // 1. Write here due to [me timerCheck] => [KNPersistentState loadFromStorage] below?!?
275 // 2. Or change call order of timerCheck, pendingApplication reminder below???
276 me.state.absentCircleWithNoReason = (circleStatus == kSOSCCCircleAbsent && reason == kSOSNeverLeftCircle);
277 [me.state writeToStorage];
278 NSLog(@"{ChangeCallback} departure reason %d", reason);
279
280 switch (reason) {
281 case kSOSDiscoveredRetirement:
282 case kSOSLostPrivateKey:
283 case kSOSWithdrewMembership:
284 case kSOSNeverAppliedToCircle:
285 break;
286
287 case kSOSNeverLeftCircle:
288 case kSOSMembershipRevoked:
289 case kSOSLeftUntrustedCircle:
290 default:
291 [me postKickedOutAlert: reason];
292 break;
293 }
294 }
295 }
296
297
298 // Circle applications: pending request(s) started / completed
299 if (me.circle.rawStatus != me.state.lastCircleStatus) {
300 SOSCCStatus lastCircleStatus = me.state.lastCircleStatus;
301 me.state.lastCircleStatus = circleStatus;
302
303 if (lastCircleStatus != kSOSCCRequestPending && circleStatus == kSOSCCRequestPending) {
304 NSLog(@"{ChangeCallback} Pending request START");
305 me.state.applicationDate = nowish;
306 me.state.pendingApplicationReminder = [me.state.applicationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]];
307 [me.state writeToStorage]; // FIXME: move below? might be needed for scheduleActivityAt...
308 [me scheduleActivityAt:me.state.pendingApplicationReminder];
309 }
310
311 if (lastCircleStatus == kSOSCCRequestPending && circleStatus != kSOSCCRequestPending) {
312 NSLog(@"Pending request completed");
313 me.state.applicationDate = [NSDate distantPast];
314 me.state.pendingApplicationReminder = [NSDate distantFuture];
315 [me.state writeToStorage];
316
317 // Remove reminders
318 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
319 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
320 if (note.userInfo[(NSString*) kValidOnlyOutOfCircleKey] && note.userInfo[@"ApplicationReminder"]) {
321 NSLog(@"{ChangeCallback} Removing notification %@", note);
322 [appropriateNotificationCenter() removeDeliveredNotification: note];
323 }
324 }
325 }
326
327 // [me.state writeToStorage];
328 }
329
330
331 // CircleJoinRequested
332 /* if (circleStatus != kSOSCCInCircle) {
333 if (circleStatus == kSOSCCRequestPending && currentAlert) { ... } */
334
335 // Clear out (old) reset notifications
336 if (me.circle.isInCircle) {
337 NSLog(@"{ChangeCallback} me.circle.isInCircle");
338 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
339 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
340 if (note.userInfo[(NSString*) kValidOnlyOutOfCircleKey]) {
341 NSLog(@"Removing existing notification (%@) now that we are in circle", note);
342 [appropriateNotificationCenter() removeDeliveredNotification: note];
343 }
344 }
345 }
346
347
348 // Applicants
349 NSLog(@"{ChangeCallback} Applicants");
350 NSMutableSet *applicantIds = [NSMutableSet new];
351 for (KDCirclePeer *applicant in me.circle.applicants) {
352 if (!me.circle.isInCircle) {
353 // Don't yammer on about circles we aren't in, and don't announce our own
354 // join requests as if the user could approve them locally!
355 break;
356 }
357 [me postForApplicant:applicant];
358 [applicantIds addObject:applicant.idString];
359 }
360
361
362 // Update notifications
363 NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter();
364 NSLog(@"Checking validity of %lu notes", (unsigned long)notificationCenter.deliveredNotifications.count);
365 for (NSUserNotification *note in notificationCenter.deliveredNotifications) {
366 if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) {
367 NSLog(@"No longer an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
368 [notificationCenter removeDeliveredNotification:note];
369 } else {
370 NSLog(@"Still an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
371 }
372 }
373
374 me.state.lastCircleStatus = me.circle.rawStatus;
375
376 [me.state writeToStorage];
377 }];
378 }
379
380
381 - (BOOL) userNotificationCenter: (NSUserNotificationCenter *) center
382 shouldPresentNotification: (NSUserNotification *) notification
383 {
384 return YES;
385 }
386
387
388 - (void) userNotificationCenter: (NSUserNotificationCenter *) center
389 didActivateNotification: (NSUserNotification *) notification
390 {
391 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
392 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]];
393 }
394 }
395
396
397 - (void) userNotificationCenter: (NSUserNotificationCenter *) center
398 didDismissAlert: (NSUserNotification *) notification
399 {
400 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Dismiss"]];
401
402 // If we don't do anything here & another notification comes in we
403 // will repost the alert, which will be dumb.
404 id applicantId = notification.userInfo[@"applicantId"];
405 if (applicantId != nil) {
406 [self.viewedIds addObject:applicantId];
407 }
408 }
409
410
411 - (void) postForApplicant: (KDCirclePeer *) applicant
412 {
413 static int postCount = 0;
414
415 if ([self.viewedIds containsObject:applicant.idString]) {
416 NSLog(@"Already viewed %@, skipping", applicant);
417 return;
418 }
419
420 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
421 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
422 if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) {
423 if (note.isPresented) {
424 NSLog(@"Already posted&presented: %@ (I=%@)", note, note.userInfo);
425 return;
426 } else {
427 NSLog(@"Already posted, but not presented: %@ (I=%@)", note, note.userInfo);
428 }
429 }
430 }
431
432 // <rdar://problem/21988060> Improve wording of the iCloud keychain drop/reset error messages
433 // Contrary to HI spec (and I think it makes more sense)
434 // 1. otherButton == top : Not Now
435 // 2. actionButton == bottom: Continue
436 // 3. If we followed HI spec, replace "Activate" => "Dismiss" in note.userInfo below
437 NSUserNotification *note = [NSUserNotification new];
438 note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_TITLE_OSX);
439 note.informativeText = [NSString stringWithFormat: (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_APPROVAL_BODY_OSX), applicant.name];
440 note._displayStyle = _NSUserNotificationDisplayStyleAlert;
441 note._identityImage = [NSImage bundleImage];
442 note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
443 note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
444 note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
445 note.identifier = [[NSUUID new] UUIDString];
446 note.userInfo = @{
447 @"applicantName": applicant.name,
448 @"applicantId" : applicant.idString,
449 @"Activate" : (__bridge NSString *) kMMPropertyKeychainAADetailsAEAction,
450 };
451
452 NSLog(@"About to post #%d/%lu (%@): %@", postCount, noteCenter.deliveredNotifications.count, applicant.idString, note);
453 [appropriateNotificationCenter() deliverNotification:note];
454 postCount++;
455 }
456
457
458 - (void) postKickedOutAlert: (int) reason
459 {
460 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
461 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
462 if (note.userInfo[(NSString*) kKickedOutKey]) {
463 if (note.isPresented) {
464 NSLog(@"Already posted&presented (removing): %@", note);
465 [appropriateNotificationCenter() removeDeliveredNotification: note];
466 } else {
467 NSLog(@"Already posted, but not presented: %@", note);
468 }
469 }
470 }
471
472 NSString *message = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_BODY_OSX);
473 if (isAppleInternal()) {
474 static const char *departureReasonStrings[] = {
475 "kSOSDepartureReasonError",
476 "kSOSNeverLeftCircle",
477 "kSOSWithdrewMembership",
478 "kSOSMembershipRevoked",
479 "kSOSLeftUntrustedCircle",
480 "kSOSNeverAppliedToCircle",
481 "kSOSDiscoveredRetirement",
482 "kSOSLostPrivateKey",
483 "unknown reason"
484 };
485 int idx = (kSOSDepartureReasonError <= reason && reason <= kSOSLostPrivateKey) ? reason : (kSOSLostPrivateKey + 1);
486 NSString *reason_str = [NSString stringWithFormat:(__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CR_REASON_INTERNAL), departureReasonStrings[idx]];
487 message = [message stringByAppendingString: reason_str];
488 }
489
490 // <rdar://problem/21988060> [ui] MONARCH: Improve wording of the iCloud keychain drop/reset error messages
491 // Contrary to HI spec (and I think it makes more sense)
492 // 1. otherButton == top : Not Now
493 // 2. actionButton == bottom: Continue
494 // 3. If we followed HI spec, replace "Activate" => "Dismiss" in note.userInfo below
495 NSUserNotification *note = [NSUserNotification new];
496 note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_PWD_REQUIRED_TITLE);
497 note.informativeText = message;
498 note._identityImage = [NSImage bundleImage];
499 note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
500 note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
501 note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
502 note.identifier = [[NSUUID new] UUIDString];
503
504 note.userInfo = @{
505 kKickedOutKey : @1,
506 kValidOnlyOutOfCircleKey: @1,
507 @"Activate" : (__bridge NSString *) kMMPropertyKeychainMRDetailsAEAction,
508 };
509
510 NSLog(@"body=%@", note.informativeText);
511 NSLog(@"About to post #-/%lu (KICKOUT): %@", noteCenter.deliveredNotifications.count, note);
512 [appropriateNotificationCenter() deliverNotification:note];
513 }
514
515
516 - (void) postApplicationReminder
517 {
518 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
519 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
520 if (note.userInfo[@"ApplicationReminder"]) {
521 if (note.isPresented) {
522 NSLog(@"Already posted&presented (removing): %@", note);
523 [appropriateNotificationCenter() removeDeliveredNotification: note];
524 } else {
525 NSLog(@"Already posted, but not presented: %@", note);
526 }
527 }
528 }
529
530 // <rdar://problem/21988060> [ui] MONARCH: Improve wording of the iCloud keychain drop/reset error messages
531 // Contrary to HI spec (and I think it makes more sense)
532 // 1. otherButton == top : Not Now
533 // 2. actionButton == bottom: Continue
534 // 3. If we followed HI spec, replace "Activate" => "Dismiss" in note.userInfo below
535 NSUserNotification *note = [NSUserNotification new];
536 note.title = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_REMINDER_TITLE_OSX);
537 note.informativeText = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_REMINDER_BODY_OSX);
538 note._identityImage = [NSImage bundleImage];
539 note._identityImageStyle = _NSUserNotificationIdentityImageStyleRectangleNoBorder;
540 note.otherButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_NOT_NOW);
541 note.actionButtonTitle = (__bridge_transfer NSString *) SecCopyCKString(SEC_CK_CONTINUE);
542 note.identifier = [[NSUUID new] UUIDString];
543
544 note.userInfo = @{
545 @"ApplicationReminder" : @1,
546 kValidOnlyOutOfCircleKey: @1,
547 @"Activate" : (__bridge NSString *) kMMPropertyKeychainWADetailsAEAction,
548 };
549
550 NSLog(@"About to post #-/%lu (REMINDER): %@ (I=%@)", noteCenter.deliveredNotifications.count, note, [note.userInfo compactDescription]);
551 [appropriateNotificationCenter() deliverNotification:note];
552 }
553
554 @end